package bf.runner;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bf.program.State;

import com.sun.org.apache.bcel.internal.Constants;

public class BF2BytecodeRunner extends ClassLoader implements Constants {
	
	public static final String BASE = "java/lang/Object";
	static int methodCount = 0;
	
	public static void main (final String args[]) throws Exception {
		System.out.println("compile and run: " + args[0]);
		long time1 = System.currentTimeMillis();
		run(new BufferedReader(new FileReader(args[0])));
		long time2 = System.currentTimeMillis();
		System.out.println();
		System.out.println(time2 - time1 + " ms");
	}

	private static void run(Reader reader) throws IOException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, SecurityException, NoSuchMethodException {
		byte[] code = compileInfo(reader);
		run(load(code));
	}

	private static byte[] compileInfo(Reader reader) throws IOException {
		byte[] code = compile(reader);
		System.out.println("Compiled code size: " + code.length + " bytes");
		return code;
	}
	
	private static byte[] compile(Reader reader) throws IOException {
		ClassWriter cw = new ClassWriter(0);
		cw.visit(Opcodes.V1_6, ACC_PUBLIC, "bf", null, BASE, null);

		genMethod(reader, cw);
		cw.visitEnd();
		
		byte[] code = cw.toByteArray();
		return code;
	}

	private static void genMethod(Reader reader, ClassWriter cw)
			throws IOException {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, getName(), "(Lbf/program/State;)V", null, null);
		mv.visitCode();
		Label l0 = new Label();
		mv.visitLabel(l0);
		
		int read;
		ParsePE:
		while((read = reader.read()) != -1) {
			char cmd = (char)read;
			switch(cmd) {
				case '>':
					genLoadValue(mv, lookAhead(reader, cmd));
					mv.visitMethodInsn(INVOKEVIRTUAL, "bf/program/State", "PADD", "(I)V");
					break;
				case '<':
					genLoadValue(mv, -lookAhead(reader, cmd));
					mv.visitMethodInsn(INVOKEVIRTUAL, "bf/program/State", "PADD", "(I)V");
					break;
				case '+':
					genLoadValue(mv, lookAhead(reader, cmd));
					mv.visitMethodInsn(INVOKEVIRTUAL, "bf/program/State", "CADD", "(I)V");
					break;
				case '-':
					genLoadValue(mv, -lookAhead(reader, cmd));
					mv.visitMethodInsn(INVOKEVIRTUAL, "bf/program/State", "CADD", "(I)V");
					break;
				case '.':
					mv.visitVarInsn(ALOAD, 0);
					mv.visitMethodInsn(INVOKEVIRTUAL, "bf/program/State", "OC", "()V");
					break;
				case '[':
					if(isClearCell(reader)){
						mv.visitVarInsn(ALOAD, 0);
						mv.visitMethodInsn(INVOKEVIRTUAL, "bf/program/State", "CLR", "()V");
					} else {
						Label l5 = new Label();
						mv.visitJumpInsn(GOTO, l5);
						Label l6 = new Label();
						mv.visitLabel(l6);
						mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
	
						methodCount++;
						mv.visitVarInsn(ALOAD, 0);
						mv.visitMethodInsn(INVOKESTATIC, "bf", getName(), "(Lbf/program/State;)V");
						genMethod(reader, cw);
						
						mv.visitLabel(l5);
						mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
						mv.visitVarInsn(ALOAD, 0);
						mv.visitMethodInsn(INVOKEVIRTUAL, "bf/program/State", "CV", "()B");
						mv.visitJumpInsn(IFNE, l6);
					}
					break;
				case ']':
					break ParsePE;
				default:
//						throw new RuntimeException("Unknown command!");
//					System.out.println("Ignore: " + cmd);
					continue;
			}
		}
		mv.visitInsn(RETURN);
		Label l9 = new Label();
		mv.visitLabel(l9);
		mv.visitLocalVariable("state", "Lbf/program/State;", null, l0, l9, 0);
		mv.visitMaxs(2, 1);
		mv.visitEnd();
	}
	
	public static void genLoadValue(MethodVisitor mv, int count) {
		mv.visitVarInsn(ALOAD, 0);
		switch(count) {
		case 0:
			mv.visitInsn(ICONST_0);
			break;
		case 1:
			mv.visitInsn(ICONST_1);
			break;
		case 2:
			mv.visitInsn(ICONST_2);
			break;
		case 3:
			mv.visitInsn(ICONST_3);
			break;
		case 4:
			mv.visitInsn(ICONST_4);
			break;
		case 5:
			mv.visitInsn(ICONST_5);
			break;
		default:
			mv.visitIntInsn(BIPUSH, count);
			break;
		}
	}

	private static boolean isClearCell(Reader reader) throws IOException {
		reader.mark(2);
		if(reader.read() == '-' && reader.read() == ']') {
			return true;
		} else {
			reader.reset();
			return false;
		}
	}
	
	private static int lookAhead(Reader reader, char cmd) throws IOException {
		int count = 1;
		while(true) {
			reader.mark(1);
			if(reader.read() != cmd) break;
			count++;
		}
		reader.reset();
		return count;
	}
	
	private static String getName() {
		String name = "run";
		if(methodCount != 0) {
			name = name.concat("" + methodCount);
		}
		return name;
	}
	
	private static Class load(byte[] code) {
		BF2BytecodeRunner loader = new BF2BytecodeRunner();
		Class exampleClass = loader.defineClass("bf", code, 0, code.length);
		return exampleClass;
	}
	
	private static void run(Class bfClass) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException {
		Method main = bfClass.getMethod("run", new Class[]{State.class});
		State state = new State();
		main.invoke(null, state);
	}

}
